﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using Prometheus.Binding.ReadOnly;
using Prometheus.TypeUtility;
using Prometheus.UIUtility;

#if !NETFX_CORE && !UNIVERSAL
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Markup;
#if !WINDOWS_PHONE
using System.Xaml;
#endif
#else
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Input;
#endif

namespace Prometheus.Binding.Core
{
    /// <summary>
    /// Base class for Prometheus binding system
    /// </summary>
    public abstract class proBindingBase
#if !WINDOWS_PHONE && !NETFX_CORE && !UNIVERSAL
#if SILVERLIGHT
        : IMarkupExtension<object>
#else
        : MarkupExtension
#endif
#endif
    {
        /// <summary>
        /// All Bindings for all forms
        /// </summary>
        internal static readonly Dictionary<proBindingBase, FrameworkElement> BoundControls =
            new Dictionary<proBindingBase, FrameworkElement>();

        private static readonly Dictionary<Type, Action<UIElement, bool, proBindingBase>>
            RegisteredControlsForBindingInitialization =
                new Dictionary<Type, Action<UIElement, bool, proBindingBase>>(10);

        private static readonly Dictionary<Type, string> RegisteredDefaultPropertyPathes =
            new Dictionary<Type, string>(10);

        protected object JustChangedValue = EventArgs.Empty;

        /// <summary>
        /// The datacontext of the UI Element
        /// </summary>
        private object _currentDataContext;

        private proBindingTriggerMode _mode;

        /// <summary>
        /// The form, which this binding belongs to.
        /// </summary>
        private proFormBase _proFormBase;

        private bool _isFormSearched;

        protected FrameworkElement uiElement;

        static proBindingBase()
        {
            RegisterDefaultPropertyPathesForBuiltInElements();
            RegisterControlInitializing();
        }

        protected proBindingBase(string elementPropertyPath, string dataContextPropertyPath)
        {
            ElementPropertyPath = elementPropertyPath;
            Path = dataContextPropertyPath;
        }

        /// <summary>
        /// Группа привязки, если string.Empty, то группируется по DataControl, в котором находится этот элемент управления
        /// </summary>
        public string BindingGroup { get; set; }

        /// <summary>
        /// Property of UI element to bind to
        /// </summary>
        public string ElementPropertyPath { get; set; }

        /// <summary>
        /// Property of the DataContext to bind to
        /// </summary>
        public string Path { get; set; }

        /// <summary>
        /// Form where element of this binding is hosted
        /// </summary>
        protected internal proFormBase ProFormBase
        {
            get
            {
                if (_proFormBase == null && !_isFormSearched)
                {
                    _isFormSearched = true;
                    if (uiElement != null) _proFormBase = uiElement.FindAncestor<proFormBase>();
                }
                return _proFormBase;
            }
        }

        /// <summary>
        /// Element in which this binding is hosted
        /// </summary>
        public virtual FrameworkElement UIElement
        {
            internal set
            {
                if(value!=null)
                if (string.IsNullOrEmpty(ElementPropertyPath))
                {
                    ElementPropertyPath = GetDefaultElementPropertyPath(value);
                }

                if (uiElement != null)
                {
                    RemoveEventHandlers();
                }

                uiElement = value;

                SetEventHandlers();

                if (value != null)
                {
                    if (ElementPropertyPath == "Visibility")
                        value.Visibility = Visibility.Collapsed;
                    if (ElementPropertyPath == "IsEnabled" && value is Control)
                    {
                        ((Control) value).IsEnabled = false;
                    }
                }
            }
            get { return uiElement; }
        }

        /// <summary>
        /// When to update binding
        /// </summary>
        public proBindingTriggerMode Mode
        {
            get { return _mode; }
            set
            {
                RemoveEventHandlers();
                _mode = value;
                SetEventHandlers();
            }
        }

        internal object CurrentDataContext
        {
            get { return _currentDataContext; }
            set
            {
                object old = _currentDataContext;
                RemoveHandlerForPropertyChangedEvent();

                _currentDataContext = value;

                SetHandlerForPropertyChangedEvent();
                CurrentDataContextChanged(old);
            }
        }

        private void RemoveEventHandlers()
        {
            if (uiElement != null)
            {
                switch (Mode)
                {
                    case proBindingTriggerMode.Default:
                        ProcessInitializationForRegisteredControl(uiElement, false);
                        break;
                    case proBindingTriggerMode.OnUpdateCall:
                        //doing nothing, owner must call update manually
                        break;
                    case proBindingTriggerMode.OnKeyUpAndLostFocus:
                        SetupToKeyUpAndLostFocus(false);
                        break;
                    case proBindingTriggerMode.OnFocusChanged:
                        SetupToKeyUpAndLostFocus(false, false);
                        break;
                }

                /*uiElement.Loaded -= onUiElement_Loaded;
                uiElement.Unloaded -= onUIElement_Unloaded;*/
#if !WINDOWS_PHONE && !NETFX_CORE && !UNIVERSAL
                uiElement.DataContextChanged -= UiElementOnDataContextChanged;
#endif
            }
        }

        /// <summary>
        /// Binding to datacontext changing, called when element loading or manually
        /// </summary>
        public virtual void Bind()
        {
            if (uiElement != null)
            {
                if (uiElement.DataContext != null)
                {
                    CurrentDataContext = uiElement.DataContext;
                }
                FromData();
            }
        }

        /// <summary>
        /// Do not use this method
        /// </summary>
        public virtual void Kill()
        {
            Unbind();

            BoundControls.Remove(this);
            _proFormBase = null;
            _isFormSearched = false;
            UIElement = null;

            FromData();
        }

        protected virtual void CurrentDataContextChanged(object oldDataContext)
        {
        }

        internal virtual void Unbind()
        {
            if (uiElement != null)
            {
                CurrentDataContext = null;
            }

            //FromData(); // why should we call FromData on UnBind ? we should not it's CPU loading
        }

#if !WINDOWS_PHONE
        private void UiElementOnDataContextChanged(object sender,
                                                   DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
        {
            Bind();
        }
#endif

        private void onUiElement_Loaded(object sender, RoutedEventArgs e)
        {
            var frameworkElement = (FrameworkElement) sender;
            BoundControls.Add(this, frameworkElement);
            UIElement = frameworkElement;

            
                if (!UIHelper.IsInDesignMode(UIElement))
                {
                    Bind();
                }
        }

        private void onUIElement_Unloaded(object sender, RoutedEventArgs e)
        {
            if (UIElement != null)
                if (!UIHelper.IsInDesignMode(UIElement))
                {
                    Kill();
                }
        }

        private void SetEventHandlers()
        {
            if (uiElement != null)
            {
                switch (Mode)
                {
                    case proBindingTriggerMode.Default:
                        ProcessInitializationForRegisteredControl(uiElement, true);
                        break;
                    case proBindingTriggerMode.OnUpdateCall:
                        //doing nothing, owner must call update manually
                        break;
                    case proBindingTriggerMode.OnKeyUpAndLostFocus:
                        SetupToKeyUpAndLostFocus(true);
                        break;
                    case proBindingTriggerMode.OnFocusChanged:
                        SetupToKeyUpAndLostFocus(true, false);
                        break;
                }

#if !WINDOWS_PHONE && !NETFX_CORE && !UNIVERSAL
                //this is firing when
                uiElement.DataContextChanged += UiElementOnDataContextChanged;
#endif
            }
        }

        internal void Follow(FrameworkElement element)
        {
            element.Loaded += onUiElement_Loaded;
            element.Unloaded += onUIElement_Unloaded;
        }

        private void SetupToKeyUpAndLostFocus(bool isInitializaing, bool setupForKeyUp = true)
        {
            if (setupForKeyUp)
            {
                if (isInitializaing)
                    uiElement.KeyUp += Element_KeyUp;
                else
                    uiElement.KeyUp -= Element_KeyUp;
            }
            if (isInitializaing)
                uiElement.LostFocus += Element_FocusChanged;
            else
                uiElement.LostFocus -= Element_FocusChanged;
        }
        
        /// <summary>
        /// Called when data need moves from source to target
        /// </summary>
        /*abstract */
        protected virtual void FromData()
        {
        }

        /// <summary>
        /// Called when data need moves from target to source
        /// </summary>
        /*abstract */
        protected virtual void ToData()
        {
        }

        /// <summary>
        /// Value cannot be EventArgs.Empty
        /// </summary>
        /// <param name="value">Value cannot be EventArgs.Empty</param>
        public void ProcessValueToData(object value)
        {
            JustChangedValue = value;
            ToData();
        }

        private void Element_FocusChanged(object sender, RoutedEventArgs e)
        {
            ToData();
        }
        
        private void Element_KeyUp(object sender,
#if !NETFX_CORE && !UNIVERSAL
            KeyEventArgs 
#else
 KeyRoutedEventArgs
#endif
            e)
        {
            ToData();
        }

        private void ProcessInitializationForRegisteredControl(FrameworkElement frameworkElement, bool isInitializaing)
        {
            Type type = frameworkElement.GetType();
            do
            {
                if (RegisteredControlsForBindingInitialization.ContainsKey(type))
                {
                    Action<UIElement, bool, proBindingBase> action = RegisteredControlsForBindingInitialization[type];
                    action(frameworkElement, isInitializaing, this);
                    return;
                }
                type = type.
#if NETFX_CORE || UNIVERSAL
GetTypeInfo().
#endif      
                    BaseType;
            } while (type != null);
            throw new InvalidOperationException("There is no default initialization action for control " +
                                                frameworkElement.GetType());
        }

        /// <summary>
        /// Register what to do with binding and control when binding is set
        /// </summary>
        /// <param name="initializationAction">This method is called when initializing is happening</param>
        /// <param name="reReregisterIfExists">Whether to reregister the control or throw an exception if control was already registered</param>
        /// <typeparam name="TElType">TYpe of the control</typeparam>
        /// <exception cref="InvalidOperationException">Thrown if control was already registered and reReregisterIfExists is "false"</exception>
        public static void RegisterControlForBindingInitialization<TElType>(
            Action<UIElement, bool, proBindingBase> initializationAction, bool reReregisterIfExists = true)
            where TElType : UIElement
        {
            Type elementType = typeof (TElType);
            if (RegisteredControlsForBindingInitialization.ContainsKey(elementType))
            {
                if (!reReregisterIfExists)
                    throw new InvalidOperationException("Element is already registered for initialization");
                RegisteredControlsForBindingInitialization.Remove(elementType);
            }
            RegisteredControlsForBindingInitialization.Add(elementType,
                                                           initializationAction);
        }

        /// <summary>
        /// Register which property of the control is the property by default for Prometheus.Binding
        /// </summary>
        /// <param name="propertyPath">Path to control property which must be bound by default</param>
        /// <param name="reReregisterIfExists">Whether to reregister the property for the control if this controls was already registered for default property</param>
        /// <typeparam name="TElType">Type of the control</typeparam>
        /// <exception cref="InvalidOperationException">Thrown if control was already registered for default property path</exception>
        public static void RegisterDefaultElementPropertyPath<TElType>(string propertyPath,
                                                                       bool reReregisterIfExists = true)
            where TElType : UIElement
        {
            Type elementType = typeof (TElType);

            if (RegisteredDefaultPropertyPathes.ContainsKey(elementType))
            {
                if (!reReregisterIfExists)
                    throw new InvalidOperationException("Element is already registered for default property path");
                RegisteredDefaultPropertyPathes.Remove(elementType);
            }

            RegisteredDefaultPropertyPathes.Add(elementType, propertyPath);
        }

        private static void RegisterControlInitializing()
        {
            RegisterControlForBindingInitialization<UIElement>(
                (control, isInitializing, binding) => binding.SetupToKeyUpAndLostFocus(isInitializing));

            RegisterControlForBindingInitialization<Selector>((control, isInitializing, binding) =>
                {
                    var selector = (Selector) control;
                    if (isInitializing)
                    {
                        selector.SelectionChanged += binding.ElementAsSelector_SelectionChanged;
                    }
                    else
                    {
                        selector.SelectionChanged -= binding.ElementAsSelector_SelectionChanged;
                    }
                }, false);

            RegisterControlForBindingInitialization<ButtonBase>((control, isInitializing, binding) =>
                {
                    var buttonBase = (ButtonBase) (control);
                    if (isInitializing)
                        buttonBase.Click += binding.Element_MouseClick;
                    else
                    {
                        buttonBase.Click -= binding.Element_MouseClick;
                    }
                });
            RegisterControlForBindingInitialization<TextBox>((element, isInitializing, binding) =>
                {
                    var textBox = (TextBox) element;
                    if(isInitializing)
                    {
                        textBox.TextChanged += binding.TextBoxOnTextChanged;
                    }
                    else
                    {
                        textBox.TextChanged -= binding.TextBoxOnTextChanged;
                    }
                });
        }

        private  void TextBoxOnTextChanged(object sender, TextChangedEventArgs e)
        {
            ToData();
        }

        private void Element_MouseClick(object sender, RoutedEventArgs e)
        {
            ToData();
        }

        private void ElementAsSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            try
            {
                var selector = (Selector) sender;
                object newValue = e.AddedItems[0].GetProperty(selector.SelectedValuePath);
                ProcessValueToData(newValue);
            }
            catch (IndexOutOfRangeException)
            {
            }
        }

        private static void RegisterDefaultPropertyPathesForBuiltInElements()
        {
            RegisterDefaultElementPropertyPath<TextBlock>("Text");
            RegisterDefaultElementPropertyPath<TextBox>("Text");
            RegisterDefaultElementPropertyPath<Selector>("SelectedValue");
            RegisterDefaultElementPropertyPath<CheckBox>("IsChecked");
            RegisterDefaultElementPropertyPath<RadioButton>("IsChecked");
            RegisterDefaultElementPropertyPath<ButtonBase>("Content");
            RegisterDefaultElementPropertyPath<Image>("Source");
        }

        private static string SearchForUIElementDefaultPropertyPath(Type type)
        {
            do
            {
                if (RegisteredDefaultPropertyPathes.ContainsKey(type))
                {
                    return RegisteredDefaultPropertyPathes[type];
                }
                type = type.
#if NETFX_CORE || UNIVERSAL
GetTypeInfo().
#endif
                    BaseType;
            } while (type != null);
            return null;
        }

        protected virtual string GetDefaultElementPropertyPath(FrameworkElement element)
        {
            Type type = element.GetType();

            string registeredPath = SearchForUIElementDefaultPropertyPath(type);
            if (registeredPath != null) return registeredPath;

            string defaultElementPath = element.GetDefaultElementPath();

            if (defaultElementPath != null)
            {
                return defaultElementPath;
            }
#if !NETFX_CORE && !UNIVERSAL
            object contProperty = type.GetCustomAttributes(typeof (ContentPropertyAttribute), true).SingleOrDefault();
            if (contProperty != null)
            {
                string defaultElementPropertyPath = ((ContentPropertyAttribute) contProperty).Name;

                if (!string.IsNullOrEmpty(defaultElementPropertyPath)) return defaultElementPropertyPath;
            }

            MemberInfo[] defaultMembers = type.GetDefaultMembers();
            foreach (MemberInfo defaultMember in defaultMembers)
            {
                if (defaultMember.MemberType == MemberTypes.Property)
                {
                    string defaultElementPropertyPath = defaultMember.Name;

                    if (!string.IsNullOrEmpty(defaultElementPropertyPath))
                        return defaultElementPropertyPath;
                }
            }
#endif
            if (UIElement != null)
                if (!UIHelper.IsInDesignMode(UIElement))
                {
                    throw new KeyNotFoundException("You must register default property for " +
                                                   type.FullName);
                }
            return null;
        }

        #region Handling data context property changed and sub property changed

        protected Dictionary<INotifyPropertyChanged, string> HandlingContext =
            new Dictionary<INotifyPropertyChanged, string>();

        private void SetHandlerForPropertyChangedEvent()
        {
            if (_currentDataContext != null)
            {
                BindPropertyChanged(_currentDataContext, Path);
            }
        }

        private void BindPropertyChanged(object currentDataContext, string path)
        {
            if (path == null) path = string.Empty;
            SimpleBindPropertyChanged(currentDataContext, path);

            int indexOf = path.IndexOf(".", StringComparison.Ordinal);
            if (indexOf == -1) return;
            string firstObjectPath = path.Substring(0, indexOf);
            object fObject = currentDataContext.GetProperty(firstObjectPath);
            string sbstr = path.Substring(indexOf + 1);
            BindPropertyChanged(fObject, sbstr);
        }

        /// <summary>
        /// Вызывается при изменении свойств или подсвойств в контексте данных
        /// </summary>
        /// <param name="sender">Объект который изменился</param>
        /// <param name="e">свойство этого объекта</param>
        private void dataContext_PropertyChanging(object sender, PropertyChangedEventArgs e)
        {
            string value;

            var notDat = sender as INotifyPropertyChanged;

            if (notDat != null)
                if (HandlingContext.TryGetValue(notDat, out value))
                {
                    if (value == e.PropertyName || string.IsNullOrEmpty(value))
                        // если в handlingcontext именно этому свойству присвоен именно этот объект//при Path="" происходил update
                    {
                        InvokeDataContextPropertyChanged(sender, e);

                        dataContext_PropertyChanged(sender, e);
                    }
                }
        }


        /// <summary>
        /// Даем право потомку определить, что делать при изменении контекста данных
        /// </summary>
        protected virtual void dataContext_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            UpdateTarget();
        }

        /// <summary>
        /// Initiate data from source to target processing
        /// </summary>
        public void UpdateTarget()
        {
            FromData();
        }

        /// <summary>
        /// Initiate data from target to source processing
        /// </summary>
        public void UpdateSource()
        {
            ToData();
        }

        /// <summary>
        /// Is called when property or subproperty at datacontext was changed
        /// </summary>
        public event EventHandler<PropertyChangedEventArgs> DataContextPropertyChanged;
        protected void InvokeDataContextPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            EventHandler<PropertyChangedEventArgs> temp = DataContextPropertyChanged;
            if (temp != null) temp(sender, e);
        }

        private void SimpleBindPropertyChanged(object currentDataContext, string path)
        {
            var notData = currentDataContext as INotifyPropertyChanged;
            if (notData != null)
            {
                notData.PropertyChanged += dataContext_PropertyChanging;
                HandlingContext.Add(notData, path);
            }
        }

        private void UnbindPropertyChanged(object currentDataContext, string path)
        {
            if (path == null) path = string.Empty;
            SimpleUnbindPropertyChanged(currentDataContext);

            int indexOf = path.IndexOf(".", StringComparison.Ordinal);
            if (indexOf == -1) return;
            string firstObjectPath = path.Substring(0, indexOf);
            object fObject = currentDataContext.GetProperty(firstObjectPath);
            string sbstr = path.Substring(indexOf + 1);
            UnbindPropertyChanged(fObject, sbstr);
        }

        private void SimpleUnbindPropertyChanged(object currentDataContext)
        {
            var notData = currentDataContext as INotifyPropertyChanged;
            if (notData != null)
            {
                notData.PropertyChanged -= dataContext_PropertyChanging;
                HandlingContext.Remove(notData);
            }
        }

        private void RemoveHandlerForPropertyChangedEvent()
        {
            if (_currentDataContext != null)
            {
                UnbindPropertyChanged(_currentDataContext, Path);
            }
        }

        #endregion

#if !WINDOWS_PHONE && !NETFX_CORE && !UNIVERSAL
        public 
#if !SILVERLIGHT
            override
#endif
            object ProvideValue(IServiceProvider serviceProvider)
        {
            var provideValueTarget = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
            object targetObject = provideValueTarget.TargetObject;
            object targetProperty = provideValueTarget.TargetProperty;

            if (targetObject is proBindingWrapper)
            {
                return this;
            }

            var targetPropertyAsProperty = (PropertyInfo)targetProperty;
            ElementPropertyPath = targetPropertyAsProperty.Name;
            proForm.SetproBinding((FrameworkElement) targetObject, this);
            return null;
        }
#endif
    }
}